package com.example.demo.service.impl;

import com.example.demo.constants.SignRedisKeyBuilder;
import com.example.demo.service.SignService;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPipelineException;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.*;

import static com.example.demo.constants.SignRedisKeyBuilder.formatDate;

@Service
@Slf4j
public class SignServiceImpl implements SignService {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    private static final String PATTERN = "yyyy-MM-dd";


    public RedisConnection getConnection() {
        return redisConnectionFactory.getConnection();
    }

    @Override
    public Boolean doSign(String userId, LocalDate localDate) {
        int offset = localDate.getDayOfMonth() - 1;
        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, localDate);
        List<Object> pipelineList;
        try {
            RedisConnection connection = getConnection();
            connection.openPipeline();

            connection.setBit(key, offset, true);
            pipelineList = connection.closePipeline();
        } catch (RedisPipelineException e) {
            log.error(String.format("用户签到错误-redis 操作错误: uid=%s, date=%s", userId, formatDate(localDate, PATTERN)), e);
            return false;
        }

        return Optional.of((Boolean) pipelineList.get(0)).orElse(Boolean.FALSE);

    }

    @Override
    public boolean checkSign(String userId, LocalDate localDate) {
        int offset = localDate.getDayOfMonth() - 1;
        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, localDate);
        try {
            RedisConnection connection = getConnection();
            Boolean isSign = connection.getBit(key, offset);
            return Optional.ofNullable(isSign).orElse(false);
        } catch (Exception e) {
            log.error(String.format("校验用户签到错误-redis 操作错误: uid=%s, date=%s", userId, formatDate(localDate, PATTERN)), e);
            return false;
        }
    }

    @Override
    public Long getSignCount(String userId, LocalDate localDate) {
        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, localDate);
        try {
            RedisConnection connection = getConnection();
            Long success = connection.bitCount(key);
            return Optional.ofNullable(success).orElse(0L);
        } catch (Exception e) {
            log.error(String.format("用户月签到统计错误-redis 操作错误: uid=%s, date=%s", userId, formatDate(localDate, PATTERN)), e);
            return 0L;
        }
    }

    @Override
    public long getContinuousSignCount(String userId, LocalDate localDate) {
        int signCount = 0;
        List<Long> list;
        int dayOfMonth = localDate.getDayOfMonth();
        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, localDate);
        final BitFieldSubCommands subCommands =
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType
                                .unsigned(dayOfMonth))
                        .valueAt(0L);
        try {
            RedisConnection connection = getConnection();
            list = connection.bitField(key, subCommands);
        } catch (Exception e) {
            log.error(String.format("用户月连续签到统计错误-redis 操作错误: uid=%s, date=%s", userId, formatDate(localDate, PATTERN)), e);
            return 0L;
        }
        if (list != null && list.size() > 0) {
            // 取低位连续不为0的个数即为连续签到次数，需考虑当天尚未签到的情况
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = 0; i < dayOfMonth; i++) {
                if (v >> 1 << 1 == v) {
                    // 低位为0且非当天说明连续签到中断了
                    if (i > 0) {
                        break;
                    }
                } else {
                    signCount += 1;
                }
                v >>= 1;
            }
        }
        return signCount;
    }

    @Override
    public LocalDate getFirstSignDate(String userId, LocalDate localDate) {
        Long pos = -1L;
        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, localDate);
        try {
            RedisConnection connection = getConnection();
            pos = connection.bitPos(key, true);
        } catch (Exception e) {
            log.error(String.format("用户月首次签到统计错误-redis 操作错误: uid=%s, date=%s", userId, formatDate(localDate, PATTERN)), e);
            return null;
        }
        return (pos == null || pos < 0) ? null : localDate.withDayOfMonth((int) (pos + 1));
    }

    @Override
    public Map<String, Boolean> getMonthSignInfo(String userId, LocalDate date) {
        List<Long> list = new ArrayList<>();
        TreeMap<String, Boolean> signMap = new TreeMap<>();
        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, date);
        final BitFieldSubCommands subCommands =
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType
                                .unsigned(date.lengthOfMonth()))
                        .valueAt(0L);
        try {
            RedisConnection connection = getConnection();
            list = connection.bitField(key, subCommands);
        } catch (Exception e) {
            log.error(String.format("获取用户当月的签到错误-redis 操作错误: uid=%s, date=%s", userId, formatDate(date, PATTERN)), e);
            return Collections.EMPTY_MAP;
        }
        fillingSignMap(date, date.lengthOfMonth(), 0, list, signMap);
        return signMap;
    }

    @Override
    public Map<String, Boolean> getSignInfoForTheLastFewDays(String userId, LocalDate date) {
        return getSignInfoForTheLastFewDays(userId, date, 7);
    }

    @Override
    public Map<String, Boolean> getSignInfoForTheLastFewDays(String uid, LocalDate date, int lastFewDays) {

        // 今天是当月的第几天
        int dayOfMonth = date.getDayOfMonth();
        TreeMap<String, Boolean> signMap = new TreeMap<>();

        // 当天在本月中的天数 >= 要获取最近 lastFewDays 天的签到情况
        if (dayOfMonth >= lastFewDays) {
            int offset = dayOfMonth - lastFewDays;
            fillingSignDetail2SignMap(lastFewDays, offset, uid, date, dayOfMonth, offset, signMap);
        }
        // 当天在本月中的天数 < 要获取最近 lastFewDays 天的签到情况, 需要从上个月获取
        else {
            fillingSignDetail2SignMapOfCrossMonth(uid, date, dayOfMonth, lastFewDays, signMap);
        }
        return signMap;
    }

    @Override
    public boolean delSignByUserIdAndDate(String userId, LocalDate date) {
        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, date);
        return del(Lists.newArrayList(key));
    }

    @Override
    public boolean batchDelSignByUserIdAndDate(Map<String, LocalDate> keyMap) {
        List<byte[]> keyList = new ArrayList<>(keyMap.size());
        Set<Map.Entry<String, LocalDate>> entries = keyMap.entrySet();
        for (Map.Entry<String, LocalDate> entry : entries) {
            byte[] key = SignRedisKeyBuilder.buildSignByteKey(entry.getKey(), entry.getValue());
            keyList.add(key);
        }
        return del(keyList);
    }

    /**
     * 从 redis 中删除指定的 keyList
     *
     * @param keyList key 列表
     * @return 已删除的 key 数量
     */
    @SuppressWarnings("RedundantThrows")
    private boolean del(List<byte[]> keyList) {
        Long result;
        byte[][] bytesKey = new byte[keyList.size()][];
        keyList.toArray(bytesKey);
        try {
            RedisConnection connection = getConnection();
            connection.del(bytesKey);
        } catch (Exception e) {
            log.error("从redis中删除指定的key-redis 操作错误: error:{})", e);
            return false;
        }
        return true;
    }


    /**
     * 获取指定天数的签到情况, 指定天数都在不在同一个月内
     *
     * @param userId      用户id
     * @param date        日期
     * @param dayOfMonth  当月的第几天
     * @param lastFewDays 获取最近几天的签到情况, 默认为 7 天
     */
    @SuppressWarnings("UnnecessaryLocalVariable")
    private void fillingSignDetail2SignMapOfCrossMonth(String userId, LocalDate date, int dayOfMonth,
                                                       int lastFewDays, Map<String, Boolean> signMap) {

        LocalDate preMonthsDate = date.minusMonths(1);
        int lengthOfPreMonth = preMonthsDate.lengthOfMonth();
        int remainingDays = lastFewDays - dayOfMonth;

        // 当月
        int currentMonthType = dayOfMonth;
        int currentMonthOffset = 0;
        int currentMonthLowDay = dayOfMonth;
        int currentMonthBeforeOfHighDay = 0;

        // 上月
        int preMonthType = remainingDays;
        int preMonthOffset = lengthOfPreMonth - remainingDays;
        int preMonthLowDay = lengthOfPreMonth;
        int preMonthBeforeOfHighDay = lengthOfPreMonth - remainingDays;


        List<Long> currentMonthList = new ArrayList<>();

        List<Long> preMonthList = new ArrayList<>();

        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, date);
        // 获取当月中 dayOfMonth 天签到情况
        final BitFieldSubCommands subCommands =
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType.unsigned(currentMonthType))
                        .valueAt(currentMonthOffset);

        byte[] preMonthKey = SignRedisKeyBuilder.buildSignByteKey(userId, preMonthsDate);

        // 获取上月中月底的 remainingDays 天签到情况
        final BitFieldSubCommands preMonthSubCommands =
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType.unsigned(preMonthType))
                        .valueAt(preMonthOffset);
        try {
            RedisConnection connection = getConnection();
            currentMonthList = connection.bitField(key, subCommands);
            preMonthList = connection.bitField(preMonthKey, preMonthSubCommands);
        } catch (Exception e) {
            log.error(String.format("获取不同月份指定天数的签到错误-redis 操作错误: uid=%s, date=%s", userId, formatDate(date, PATTERN)), e);
            return;
        }

        // 当月
        fillingSignMap(date, currentMonthLowDay, currentMonthBeforeOfHighDay, currentMonthList, signMap);
        // 上月
        fillingSignMap(preMonthsDate, preMonthLowDay, preMonthBeforeOfHighDay, preMonthList, signMap);
    }


    /**
     * 获取指定天数的签到情况, 指定天数都在同一个月内
     *
     * @param type            无符号的位数
     * @param offset          偏移量
     * @param userId          用户id
     * @param date            日期
     * @param lowDay          低位数(DayOfMonth)
     * @param beforeOfHighDay 高位数(DayOfMonth)
     */
    private void fillingSignDetail2SignMap(int type, int offset, String userId, LocalDate date,
                                           int lowDay, int beforeOfHighDay, Map<String, Boolean> signMap) {

        List<Long> list;
        byte[] key = SignRedisKeyBuilder.buildSignByteKey(userId, date);
        final BitFieldSubCommands subCommands =
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType.unsigned(type))
                        .valueAt(offset);
        try {
            RedisConnection connection = getConnection();
            list = connection.bitField(key, subCommands);
        } catch (Exception e) {
            log.error(String.format("获取同一月份指定天数的签到错误-redis 操作错误: uid=%s, date=%s", userId, formatDate(date, PATTERN)), e);
            return;
        }
        fillingSignMap(date, lowDay, beforeOfHighDay, list, signMap);
    }


    /**
     * 把 list 数据注入到 signMap 中
     *
     * @param date            date 与 lowDay 和 beforeOfHighDay 有对应关系
     * @param lowDay          低位数(DayOfMonth)
     * @param beforeOfHighDay 高位数(DayOfMonth)
     * @param list            签到的 bit 数据
     */
    private void fillingSignMap(LocalDate date, int lowDay, int beforeOfHighDay, List<Long> list, Map<String, Boolean> signMap) {
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        // 由低位到高位，为0表示未签到，为1表示已签到
        long v = list.get(0) == null ? 0 : list.get(0);
        for (int i = lowDay; i > beforeOfHighDay; i--) {
            LocalDate d = date.withDayOfMonth(i);
            signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
            v >>= 1;
        }
    }

}
